silver - 3 | 75 lp (including bronze 3/unranked)
silver - 1 | 52 lp (excluding bronze 3/unranked)
## Rows: 11521 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): match_uid, gamemode
## dbl (1): replay_id
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
match_players <- match_players %>%
filter(hero_data_playtime >= 30) # Remove short swaps
match_players <- match_players %>%
group_by(match_uid) %>%
mutate(
is_draw = all(is_win == FALSE) # If no one won, it's a draw
) %>%
ungroup()
# Step 2: Calculate each hero's percentage of the total match duration per player
match_players <- match_players %>%
group_by(match_uid, player_uid) %>%
mutate(
playtime_percentage = hero_data_playtime / sum(hero_data_playtime)
) %>%
ungroup()
# Step 3: Merge hero names
match_players <- match_players %>%
mutate(hero_data_hero_id = as.character(hero_data_hero_id)) %>%
left_join(hero_stats, by = c("hero_data_hero_id" = "hero_data_hero_id"))
# Step 4: Create team compositions
team_comps <- match_players %>%
group_by(match_uid, is_win, player_uid) %>%
summarise(heroes = paste(sort(unique(name.y)), collapse = "/"), .groups = "drop") %>%
group_by(match_uid, is_win) %>%
summarise(team = paste(sort(unique(heroes)), collapse = ", "), .groups = "drop")
# Step 5: Match each team against its opponent
matchups <- team_comps %>%
pivot_wider(names_from = is_win, values_from = team, names_prefix = "team_") %>%
filter(!is.na(team_TRUE) & !is.na(team_FALSE)) # Ensure both teams exist
# Step 6: Compute individual hero matchups (with playtime adjustments)
hero_matchups <- match_players %>%
select(match_uid, player_uid, name.y, playtime_percentage, is_win, is_draw) %>%
rename(hero = name.y)
# Step 7: Join the opposing team members per match (ensuring only enemy matchups)
hero_matchups <- hero_matchups %>%
inner_join(
hero_matchups %>%
rename(opponent_hero = hero, opponent_playtime = playtime_percentage, is_win_opponent = is_win, player_uid_opponent = player_uid),
by = c("match_uid"),
suffix = c("_self", "_opponent")
) %>%
select(-is_draw_opponent) %>%
filter(
hero != opponent_hero, # Remove self-matches
player_uid != player_uid_opponent, # Ensure we're not matching the same player
is_win != is_win_opponent, # Ensure we're only comparing opponents
!is_draw_self # Exclude draws
)
## Warning in inner_join(., hero_matchups %>% rename(opponent_hero = hero, : Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 1 of `x` matches multiple rows in `y`.
## ℹ Row 1 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
## "many-to-many"` to silence this warning.
hero_matchups <- hero_matchups %>% select(-is_draw_self)
# Step 8: Compute partial wins/losses per hero matchup
hero_matchups <- hero_matchups %>%
mutate(
weighted_win = ifelse(is_win, playtime_percentage * opponent_playtime, 0),
weighted_loss = ifelse(!is_win, playtime_percentage * opponent_playtime, 0)
)
# Step 9: Aggregate hero counter data
hero_counters <- hero_matchups %>%
group_by(hero, opponent_hero) %>%
summarise(
wins = sum(weighted_win),
losses = sum(weighted_loss),
total_matches = wins + losses,
win_rate = (wins / total_matches) * 100,
.groups = "drop"
) %>%
arrange(desc(win_rate))
# Step 10: Apply a threshold to filter out rare matchups
total_matches <- nrow(matches)
threshold <- total_matches * 0.01 # 1% of all matches
hero_counters <- hero_counters %>%
filter(total_matches >= threshold)
# Step 11: Compute overall hero performance (with playtime adjustments)
hero_stats_counters <- hero_matchups %>%
group_by(hero) %>%
summarise(
total_wins = sum(weighted_win),
total_losses = sum(weighted_loss),
total_matches = total_wins + total_losses,
win_rate = (total_wins / total_matches) * 100,
.groups = "drop"
)
# Step 12: Merge role and icon data for visualization
hero_counters <- hero_counters %>%
left_join(select(all_heroes, name, role, icon), by = c("hero" = "name")) %>%
rename(role_win = role, icon_win = icon) %>%
left_join(select(all_heroes, name, role, icon), by = c("opponent_hero" = "name")) %>%
rename(role_lose = role, icon_lose = icon)
library('ggforce')
# Convert data to long format for ggplot
# Table Format
# Table Format with Icons and Role-based Structure
get_top_counters <- function(df, hero_name, role, strongest = TRUE) {
filtered_df <- df %>%
filter(hero == hero_name & role_lose == role) %>%
arrange(if (strongest) desc(win_rate) else win_rate) %>%
head(3) # Select top 3
if (nrow(filtered_df) == 0) return("") # Return empty if no matchups
return(paste0(
'<div style="display: inline-block; text-align: center; margin: 5px;">
<div style="font-size: 12px; font-weight: bold; color: white; background: ',
ifelse(100-filtered_df$win_rate > 50, 'green', 'red'),
'; padding: 2px; border-radius: 5px;">',
round(100-filtered_df$win_rate, 1), '%</div>
<img src="', filtered_df$icon_lose, '" height="30px" style="display: block; margin-top: 2px;">
</div>',
collapse = " "
))
}
# Create table with strongest & weakest counters per role for each hero
hero_counters_table <- hero_counters %>%
distinct(hero, role_win, icon_win) %>%
rowwise() %>%
mutate(
strongest_vanguard_counters = get_top_counters(hero_counters, hero, "VANGUARD", FALSE),
strongest_duelist_counters = get_top_counters(hero_counters, hero, "DUELIST", FALSE),
strongest_strategist_counters = get_top_counters(hero_counters, hero, "STRATEGIST", FALSE),
weakest_vanguard_counters = get_top_counters(hero_counters, hero, "VANGUARD", TRUE),
weakest_duelist_counters = get_top_counters(hero_counters, hero, "DUELIST", TRUE),
weakest_strategist_counters = get_top_counters(hero_counters, hero, "STRATEGIST", TRUE)
) %>%
ungroup()
# Merge hero_stats win_rate into hero_counters_table
hero_counters_table <- hero_counters_table %>%
left_join(hero_stats %>% select(name, win_rate, pick_rate), by = c("hero" = "name")) %>%
arrange(desc(win_rate)) %>%
mutate(
pick_rate = paste0(round(pick_rate,2),'%'),
win_rate = paste0(win_rate,'%'),
)
# Convert heroes_win to image format for display
hero_counters_table <- hero_counters_table %>%
mutate(icon_win = paste0(
'<div style="position: relative; display: inline-block;">
<img src="', icon_win, '" height="50px" style="border-radius: 10px;">
<div style="position: absolute; bottom: 4px; left: 0px; background: rgba(0, 0, 0, 0.5); border-radius: 3px; padding: 1px 3px; height: 18px; display: flex; align-items: center;">
<img src="https://rivalskins.com/wp-content/uploads/marvel-assets/ui/roles/', tolower(role_win), '.png"
height="14px" style="display: block;">
</div>
</div>'
))
hero_counters_table <- hero_counters_table %>%
rename("Hero" = icon_win,
"Winrate" = win_rate,
"Pickrate" = pick_rate,
"Best Tank to Pick" = strongest_vanguard_counters,
"Best Dps to Pick" = strongest_duelist_counters,
"Best Support to Pick" = strongest_strategist_counters,
"Worst Tank to Pick" = weakest_vanguard_counters,
"Worst Dps to Pick" = weakest_duelist_counters,
"Worst Support to Pick" = weakest_strategist_counters,
)
# Create interactive datatable
datatable(hero_counters_table, escape = FALSE, options = list(pageLength = 10, autoWidth = TRUE, columnDefs = list(
list(targets = c(1,2), visible = FALSE) # Adjust these numbers based on step 1
))) %>%
formatStyle(
columns = names(hero_counters_table), # Apply to all columns
`text-align` = "center" # Center-align text
)
library(dplyr)
library(tidyr)
library(cluster) # For hierarchical clustering
library(dbscan) # For DBSCAN
library(stringr)
# Ensure team compositions contain exactly 6 heroes (and no duplicates)
team_comps <- match_players %>%
group_by(match_uid, is_win, player_uid) %>%
summarise(heroes = sort(unique(name.y)), .groups = "drop") %>%
filter(length(heroes) == 6) %>% # Only allow valid 6-hero compositions
mutate(team = paste(heroes, collapse = "/")) %>%
group_by(team) %>%
summarise(
win_rate = mean(is_win) * 100,
games_played = n(),
.groups = "drop"
) %>%
arrange(desc(games_played))
# Convert teams into binary feature vectors (one-hot encoding)
hero_list <- unique(unlist(strsplit(paste(team_comps$team, collapse = "/"), "/")))
hero_matrix <- do.call(rbind, lapply(team_comps$team, function(t) {
as.integer(hero_list %in% unlist(strsplit(t, "/")))
}))
colnames(hero_matrix) <- hero_list
# Perform DBSCAN clustering
dbscan_result <- dbscan(hero_matrix, eps = 1.5, minPts = 3) # Adjust eps for better grouping
team_comps$cluster <- dbscan_result$cluster
# View cluster assignments
team_comps %>% arrange(cluster, desc(games_played))
Created by